#!/usr/bin/env python
#   vim: fileencoding=utf-8

# This file is part of open-hea.
#
# open-hea is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# open-hea is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with open-hea.  If not, see <http://www.gnu.org/licenses/>.

__doc__ = """
This module contains utilities of use both to test fixtures and
the main application.

"""

import contextlib
import logging
import os
import platform
import sys


class SqliteAdapter(object):
    """
    Provides utilities for checking the capabilities of the local SQLite
    library and its Python interfaces.
    """
    # Some compatibility issues remain on Mac OSX, see:
    # http://www.maurobianchi.it/blog/2012/03/16/geodjango-spatialite-osx/

    __shared_state = {}

    BASE = "base"
    FK = "foreign keys"
    GEO = "geo"

    def __init__(self):
        self.__dict__ = self.__shared_state

    @property
    def capabilities(self):
        if hasattr(self, "_caps"):
            return self._caps

        caps = {self.BASE: set(), self.FK: set(), self.GEO: set()}
        for check, feature in (
            (self.sqlite3_check_import, self.BASE),
            (self.sqlite3_check_fks, self.FK),
            (self.pysqlite_check_import, self.BASE),
            (self.pysqlite_check_extensions, self.GEO),
            (self.pysqlite_load_extensions, self.GEO)
        ):
            mod, reason = check()
            self.record(caps, feature, mod, reason)

        modules = [i for i in caps[self.BASE] if type(i) is type(logging)]
        for mod, reason in self.pragma_check_fks(modules):
            self.record(caps, self.FK, mod, reason)

        self._caps = caps
        return self._caps

    def discover(self, features=[BASE]):
        """ Return a SQLite module which supports the required features """
        logging.debug("features: %s" % ", ".join(features))

        caps = self.capabilities
        sets = [caps[i] for i in features if i in caps]
        try:
            rv = [i for i in sets[0].intersection(*sets[1:])
                  if type(i) is type(logging)][0]
        except IndexError:
            rv = None
        logging.debug("recommending: %r" % rv)
        return rv

    def diagnosis(self, features=[FK, GEO], debug=False):
        """ Return a sequence of reasons why features could not be provided """
        caps = self.capabilities
        sets = [caps[i] for i in features if i in caps]
        rv = []
        if debug or not self.discover(features):
            # Supply all diagnostic errors
            rv = [i for i in set().union(*sets)
                  if type(i) is not type(logging)]
        return rv

    @staticmethod
    def record(caps, feature, mod, reason):
        s = caps[feature]
        if not reason:
            s.add(mod)
        else:
            s.discard(mod)
            s.add(reason)

    @staticmethod
    def sqlite3_check_import():
        """ Check that the SQLite3 library is available. """
        try:
            import sqlite3
        except ImportError as e:
            return (None, str(e))
        else:
            return (sqlite3, "")

    @staticmethod
    def sqlite3_check_fks():
        """
        Check that the underlying SQLite3 library is new enough to
        support foreign key constraints.
        """
        sqlite3, reason = SqliteAdapter.sqlite3_check_import()
        if sqlite3 is None:
            return (None, reason)

        required = [3, 6, 19]
        if [int(i) for i in sqlite3.sqlite_version.split('.')] < required:
            reason = ("sqlite version %d.%d.%d or higher is required" %
                      tuple(required))
            return (sqlite3, reason)

        return (sqlite3, "")

    @staticmethod
    def pysqlite_check_import():
        """
        Check if the pysqlite2 library is available.
        """
        try:
            import pysqlite2.dbapi2
        except ImportError:
            return (None, "Couldn't find the pysqlite module")
        else:
            return (pysqlite2.dbapi2, "")

    @staticmethod
    def pysqlite_check_extensions():
        """
        Check if the pysqlite2 library permits extension loading.
        See http://www.geoalchemy.org/usagenotes.html for details.
        """
        pysqlite, reason = SqliteAdapter.pysqlite_check_import()
        if pysqlite is None:
            return (None, reason)

        if platform.system() in ("Linux", "Windows"):
            con = pysqlite.connect(":memory:")
            try:
                con.enable_load_extension(True)
            except AttributeError as e:
                return (None,
                        "Pysqlite must be compiled with "
                        "'SQLITE_OMIT_LOAD_EXTENSION' disabled")
            else:
                return (pysqlite, "")
            finally:
                con.close()
        else:
            return (pysqlite, "")

    @staticmethod
    def spacialite_load_extensions(con):
        """
        Load the Spacialite extensions into SQLite.
        """
        error = None

        # Spacialite is noisy. This approach to
        # suppressing output works both with 'unittest discover'
        # and Nose (which assigns sys.stdout to a StringIO
        # object, complicating things).
        devnull = open(os.path.devnull, 'w')
        stdoutFd = os.dup(sys.__stdout__.fileno())
        os.dup2(devnull.fileno(), 1)

        if platform.system() == "Linux":
            try:
                con.enable_load_extension(True)
                con.load_extension("libspatialite.so.3")
            except:
                error = ("libspatialite.so.3 not in path. "
                         "Try installing spacialite-bin.")
        elif platform.system() == "Windows":
            try:
                con.enable_load_extension(True)
                con.load_extension("libspatialite-3.dll")
            except:
                error = "Cannot find libspatialite-3.dll in path."
        elif platform.system() == "Darwin":
            try:
                cur = con.cursor()
                cur.execute("SELECT load_extension(\"libspatialite.dylib\")")
            except:
                error = "Cannot load libspatialite.dylib in path."

        # return to standard output
        os.dup2(stdoutFd, 1)
        os.close(stdoutFd)
        devnull.close()

        return error

    @staticmethod
    def spacialite_init(con):
        """
        Initialise the Spacialite database with a reference ellipsoid
        """
        con.executescript("""SELECT InitSpatialMetaData();
            INSERT OR IGNORE INTO spatial_ref_sys
            (srid, auth_name, auth_srid, ref_sys_name, proj4text)
            VALUES (4326, 'epsg', 4326, 'WGS 84',
            '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs');
            """)

    @staticmethod
    def pysqlite_load_extensions():
        """
        Check if the pysqlite2 library extension can be loaded.
        See http://code.google.com/p/maps4mac/issues/detail?id=3
        """
        pysqlite, reason = SqliteAdapter.pysqlite_check_import()
        if pysqlite is None:
            return (None, reason)

        con = pysqlite.connect(":memory:")
        error = SqliteAdapter.spacialite_load_extensions(con)
        con.close()

        if error:
            return (pysqlite, error)
        else:
            return (pysqlite, "")

    @staticmethod
    def pragma_check_fks(modules=[]):
        """
        Checks if the supplied modules are able to activate foreign key
        constraints.
        """
        for mod in modules:
            with contextlib.closing(mod.connect(":memory:")) as con:
                if not con.execute("pragma foreign_keys").fetchone():
                    reason = "sqlite foreign keys are not a compiled option"
                    yield (mod, reason)
                else:
                    yield (mod, "")
